package com.googlecode.afx.view.fxml;

import java.lang.reflect.Field;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javafx.scene.Node;

import com.googlecode.afx.annotation.AFXBinding;
import com.googlecode.afx.utils.AFXExpressionUtils;
import com.googlecode.afx.utils.AFXUtils;
import com.googlecode.afx.utils.ReflectionUtils;
import com.googlecode.afx.view.AbstractFxmlView;
import com.googlecode.afx.view.NodeWrapper;
import com.googlecode.afx.view.View;
import com.googlecode.afx.view.binding.BindingCapableNodeHandler;

/**
 * Abstract base class for views that shall also support a databinding of JavaBeans.
 * 
 * @author Martin
 *
 */
public abstract class AbstractFxmlDatabindingView extends AbstractFxmlView {

	private static final Log LOG = LogFactory.getLog(AbstractFxmlDatabindingView.class);

	private ModelHolder modelHolder;
	
	/**
	 * Performs  data binding with the given model object.
	 * 
	 * @param model
	 */
	public void initBindings() {
		if(this.modelHolder == null) {
			LOG.debug("Attribute 'modelHolder' is not set. Skip binding.");
			return;
		}
		this.initBindingsFromFXMLToModelHolder();
		this.initBindingsFromModelHolderToFXML();
	}

	/**
	 * Init all bindings defined in the FXML file. Binding is established by setting the "id" attribute in <tt>Node</tt> in that way so that
	 * it points to a field name with the same name than specified in "id".
	 */
	protected void initBindingsFromFXMLToModelHolder() {
		LOG.debug("Initialize databindings from FXML view '" + this.getFxml() + "' to model holder '"+ this.modelHolder.getModel().getClass().getName() +"'.");
		
		Node node = this.getNode();
		BindingCapableNodeHandler handler = new BindingCapableNodeHandler();
		NodeWrapper.traverse(node, handler);
		Map<String, Node> nodeMap = handler.getNodeMap();;
		
		for(Map.Entry<String, Node> entry : nodeMap.entrySet()) {
			
			// check, if a binding with the given id is possible
			if(this.modelHolder.isReadableProperty(entry.getKey())) {
				this.modelHolder.bind(entry.getKey(), entry.getValue());
			}
		}
	}
	
	/**
	 * Init bindings that are defined within the <tt>modelHolder</tt> instance via the <tt>AFXBinding</tt> annotation.
	 */
	protected void initBindingsFromModelHolderToFXML() {
		LOG.debug("Initialize databindings from model holder '"+ this.modelHolder.getModel().getClass().getName() +"' to FXML view '" + this.getFxml() + "'.");
		
		Map<Field, AFXBinding> fieldMap = ReflectionUtils.findAnnotatedFields(this.modelHolder.getClass(), AFXBinding.class); 
		if(fieldMap != null && !fieldMap.isEmpty()) {
			for(Map.Entry<Field, AFXBinding> entry : fieldMap.entrySet()) {
				String path = this.getRelativeNestedPath(entry.getKey(), entry.getValue().path());
				String componentId = entry.getValue().componentId();
				String propertyMethod = entry.getValue().propertyMethod();
								
				
				View view = (View) this.determineComponent(componentId, this);
				if(view == null) {
					LOG.warn("Can not retrieve view based on the supplied attribute AFXBinding.componentId='" + componentId + "'! Please check your annotation!");
					continue;
				}
				Node nodeToBind = AFXUtils.findNodeById(view.getNode(), this.determineNodeId(componentId));
				if(nodeToBind == null) {
					LOG.warn("Can not retrieve node based on the supplied attribute AFXBinding.componentId='" + componentId + "'! Please check your annotation!");
					continue; 
				}
				
				this.modelHolder.bind(path, entry.getValue().elementType(), nodeToBind, propertyMethod);
			}
		}
	}
	
	public void setModel(Object model) {
		this.modelHolder = new ModelHolder(model);
	}

	public Object getModel() {
		return this.modelHolder != null ? this.modelHolder.getModel() : null;
	}
	
	/**
	 * Determines the correct component defined by the supplied <tt>path</tt>. 
	 * If <tt>path</tt> is a nested path with dot-notation, the first name in the path 
	 * is supposed to be bean name that is retrieved from the component cache. 
	 * 
	 * @param path
	 * @param defaultComponent
	 * @return
	 */
	protected Object determineComponent(String path, Object defaultComponent) {
		Object retVal = defaultComponent;
		String baseName = AFXExpressionUtils.getComponentName(path);
		if(baseName != null) {
			retVal = this.getViewCache().lookupComponent(baseName);
		}
		return retVal;
	}

	/**
	 * Determines the correct method name defined by the supplied <tt>path</tt>. 
	 * If <tt>path</tt> is a nested path with dot-notation, the second name in the path 
	 * is supposed to be the method name. 
	 * 
	 * @param path
	 * @param controller
	 * @return
	 */
	protected String determineMethodName(String path) {
		String methodName = AFXExpressionUtils.getMethodName(path);
		return methodName;
	}	
	

	/**
	 * Determines the correct node ID defined by the supplied <tt>path</tt>. 
	 * If <tt>path</tt> is a nested path with dot-notation, the second name in the path 
	 * is supposed to be the node ID. 
	 * 
	 * @param path
	 * @param controller
	 * @return
	 */
	protected String determineNodeId(String path) {
		String nodeId = AFXExpressionUtils.getNodeId(path);
		return nodeId;
	}	
	
	/**
	 * Determines the nested path based on the <tt>field</tt> and the <tt>path</tt> attribute.
	 * @param field
	 * @param path
	 * @return
	 */
	protected String getRelativeNestedPath(Field field, String path) {
		// construct nested path to lookup values relative to the annotated field
		return (StringUtils.trimToNull(path) == null) ? field.getName() : field.getName() + "." + path;
	}

	public void show() {
		this.initBindings();
		this.showAfterBind();
	}
	
	/**
	 * Abstract method to override by views that display the view after bindings have been applied.
	 */
	public abstract void showAfterBind();
}
